package modbus

import (
	"errors"
	"fmt"
	"log"
	"os"
	"strings"
	"time"

	"github.com/project-flogo/core/data/coerce"

	"github.com/project-flogo/core/data/metadata"

	"github.com/mitchellh/mapstructure"

	"github.com/masterclock/modbus"
	"github.com/project-flogo/core/activity"
)

type tcpClientParams struct {
	Addr    string        `md:"addr,required" json:"addr,omitempty"`
	Timeout time.Duration `md:"timeout" json:"timeout,omitempty"`
	SlaveID int           `md:"slaveId" json:"slaveId,omitempty"`
}

type rtuClientParams struct {
	Addr     string        `md:"addr,required" json:"addr,omitempty"`
	Timeout  time.Duration `md:"timeout" json:"timeout,omitempty"`
	SlaveID  int           `md:"slaveId" json:"slaveId,omitempty"`
	Baudrate int           `md:"baudrate" json:"baudrate,omitempty"`
	DataBits int           `md:"dataBits" json:"dataBits,omitempty"`
	Parity   string        `md:"parity" json:"parity,omitempty"`
	StopBits int           `md:"stopBits" json:"stopBits,omitempty"`
}

type settings struct {
	clientType string
	tcpParams  tcpClientParams
	rtuParams  rtuClientParams
}

type opFunction func(ctx activity.Context, input *Input) ([]byte, error)

type Activity struct {
	settings    settings
	clientReady bool
	client      modbus.Client
	opFunctions map[string]opFunction
}

func init() {
	_ = activity.Register(&Activity{}, New)
}

func New(ctx activity.InitContext) (activity.Activity, error) {
	settings, err := convertSettings(ctx.Settings())
	if err != nil {
		return nil, err
	}
	act := &Activity{
		settings: *settings,
	}
	act.opFunctions["READCOILS"] = act.evalReadCoils
	act.opFunctions["READDISCRETEINPUTS"] = act.evalReadDiscreteInputs
	act.opFunctions["WRITESINGLECOIL"] = act.evalWriteSingleCoil
	act.opFunctions["WRITEMULTIPLECOILS"] = act.evalWriteMultipleCoils
	act.opFunctions["READINPUTREGISTERS"] = act.evalReadInputRegisters
	act.opFunctions["READHOLDINGREGISTERS"] = act.evalReadHoldingRegisters
	act.opFunctions["WRITESINGLEREGISTER"] = act.evalWriteSingleRegister
	act.opFunctions["WRITEMULTIPLEREGISTERS"] = act.evalWriteMultipleRegisters
	act.opFunctions["READWRITEMULTIPLEREGISTERS"] = act.evalReadWriteMultipleRegisters
	act.opFunctions["MASKWRITEREGISTER"] = act.evalMaskWriteRegister
	act.opFunctions["READFIFOQUEUE"] = act.evalReadFIFOQueue
	return act, nil
}

var activityMd = activity.ToMetadata(&Settings{}, &Input{}, &Output{})

func (a *Activity) Metadata() *activity.Metadata {
	return activityMd
}

func (a *Activity) Eval(ctx activity.Context) (bool, error) {
	var err error
	if !a.clientReady {
		a.client, err = a.createClient(ctx)
		if err != nil {
			return true, err
		}
		a.clientReady = true
	}

	input := &Input{}
	err = ctx.GetInputObject(input)
	if err != nil {
		return true, err
	}

	var results []byte
	opName := strings.ToUpper(strings.TrimSpace(input.Op))
	if opFunc, ok := a.opFunctions[opName]; ok {
		results, err = opFunc(ctx, input)
	} else {
		err = fmt.Errorf("invalid op: %s", input.Op)
	}

	errStr := ""
	if err != nil {
		errStr = err.Error()
	}
	output := &Output{
		Results: results,
		Err:     errStr,
	}
	err = ctx.SetOutputObject(output)
	return true, err
}

func convertSettings(m map[string]interface{}) (*settings, error) {
	var s Settings
	err := metadata.MapToStruct(m, &s, true)
	if err != nil {
		return nil, err
	}

	var settings settings
	if s.ClientType == "" || strings.ToUpper(s.ClientType) == "TCP" {
		settings.clientType = "TCP"
		params, err := mapToTCPClientParams(s.Params)
		if err != nil {
			return nil, err
		}
		settings.tcpParams = params
	} else if strings.ToUpper(s.ClientType) == "RTU" {
		settings.clientType = "RTU"
		params, err := mapToRTUClientParams(s.Params)
		if err != nil {
			return nil, err
		}
		settings.rtuParams = params
	} else {
		return nil, fmt.Errorf("invalid client type: %s", s.ClientType)
	}
	return &settings, nil
}

func mapToTCPClientParams(m map[string]interface{}) (tcpClientParams, error) {
	var timeout time.Duration
	if tAny, ok := m["timeout"]; ok {
		tStr, err := coerce.ToString(tAny)
		if err != nil {
			return tcpClientParams{}, err
		}
		timeout, err = time.ParseDuration(tStr)
		if err != nil {
			return tcpClientParams{}, err
		}
	}
	delete(m, "timeout")

	params := tcpClientParams{}
	err := metadata.MapToStruct(m, &params, true)
	if err != nil {
		return tcpClientParams{}, err
	}
	params.Timeout = timeout
	return params, nil
}

func mapToRTUClientParams(m map[string]interface{}) (rtuClientParams, error) {
	var timeout time.Duration
	if tAny, ok := m["timeout"]; ok {
		tStr, err := coerce.ToString(tAny)
		if err != nil {
			return rtuClientParams{}, err
		}
		timeout, err = time.ParseDuration(tStr)
		if err != nil {
			return rtuClientParams{}, err
		}
	}
	delete(m, "timeout")

	params := rtuClientParams{}
	err := metadata.MapToStruct(m, &params, true)
	if err != nil {
		return rtuClientParams{}, err
	}
	params.Timeout = timeout
	return params, nil
}

func normTCPClientParams(params tcpClientParams) (tcpClientParams, error) {
	if params.Timeout == 0 {
		params.Timeout = time.Second * 10
	}
	if params.SlaveID == 0 {
		params.SlaveID = 0xFF
	}
	if params.Addr == "" {
		return params, errors.New("invalid connection params, must provide URL")
	}
	return params, nil
}

func normRTUClientParams(params rtuClientParams) (rtuClientParams, error) {
	if params.Timeout == 0 {
		params.Timeout = time.Second * 10
	}
	if params.SlaveID == 0 {
		params.SlaveID = 0xFF
	}
	if params.Addr == "" {
		return params, errors.New("invalid connection params, must provide device")
	}
	if params.Baudrate == 0 {
		params.Baudrate = 2400
	}
	if params.DataBits == 0 {
		params.DataBits = 8
	}
	parity := strings.ToUpper(strings.TrimSpace(params.Parity))
	if parity == "" {
		params.Parity = "N"
	} else if parity == "NONE" {
		params.Parity = "N"
	} else if parity == "EVEN" {
		params.Parity = "E"
	} else if parity == "ODD" {
		params.Parity = "O"
	} else {
		return params, fmt.Errorf("invalid parity: %s", params.Parity)
	}
	if params.StopBits <= 0 {
		params.StopBits = 1
	} else if params.StopBits > 2 {
		return params, fmt.Errorf("invalid stop bits: %d", params.StopBits)
	}
	return params, nil
}

func (a *Activity) createClient(ctx activity.Context) (modbus.Client, error) {
	switch a.settings.clientType {
	case "TCP":
		{
			return a.createTCPClient(ctx, a.settings.tcpParams)
		}
	case "RTU":
		{
			return a.createRTUClient(ctx, a.settings.rtuParams)
		}
	default:
		{
			return nil, fmt.Errorf("invalid client type: %s", a.settings.clientType)
		}
	}
}

func (a *Activity) createTCPClient(ctx activity.Context, params tcpClientParams) (modbus.Client, error) {
	handler := modbus.NewTCPClientHandler(params.Addr)
	handler.Timeout = params.Timeout
	handler.SlaveId = uint8(params.SlaveID)
	handler.Logger = log.New(os.Stdout, "modbus:activity: ", log.LstdFlags)
	err := handler.Connect()
	if err != nil {
		return nil, err
	}
	client := modbus.NewClient(handler)
	return client, nil
}

func (a *Activity) createRTUClient(ctx activity.Context, params rtuClientParams) (modbus.Client, error) {
	handler := modbus.NewRTUClientHandler(params.Addr)
	handler.BaudRate = params.Baudrate
	handler.DataBits = params.DataBits
	handler.Parity = params.Parity
	handler.StopBits = params.StopBits
	handler.SlaveId = uint8(params.SlaveID)
	handler.Timeout = params.Timeout
	handler.Logger = log.New(os.Stdout, "modbus:activity: ", log.LstdFlags)
	err := handler.Connect()
	if err != nil {
		return nil, err
	}
	client := modbus.NewClient(handler)
	return client, nil
}

func (a *Activity) evalReadCoils(ctx activity.Context, input *Input) ([]byte, error) {
	params := ReadCoilsParams{}
	err := mapstructure.Decode(input.Params, &params)
	if err != nil {
		return nil, err
	}
	return a.client.ReadCoils(params.Address, params.Quantity)
}

func (a *Activity) evalReadDiscreteInputs(ctx activity.Context, input *Input) ([]byte, error) {
	params := ReadDiscreteInputs{}
	err := mapstructure.Decode(input.Params, &params)
	if err != nil {
		return nil, err
	}
	return a.client.ReadDiscreteInputs(params.Address, params.Quantity)
}

func (a *Activity) evalWriteSingleCoil(ctx activity.Context, input *Input) ([]byte, error) {
	params := WriteSingleCoil{}
	err := mapstructure.Decode(input.Params, &params)
	if err != nil {
		return nil, err
	}
	return a.client.WriteSingleCoil(params.Address, params.Value)
}

func (a *Activity) evalWriteMultipleCoils(ctx activity.Context, input *Input) ([]byte, error) {
	params := WriteMultipleCoils{}
	err := mapstructure.Decode(input.Params, &params)
	if err != nil {
		return nil, err
	}
	return a.client.WriteMultipleCoils(params.Address, params.Quantity, params.Value)
}

func (a *Activity) evalReadInputRegisters(ctx activity.Context, input *Input) ([]byte, error) {
	params := ReadInputRegisters{}
	err := mapstructure.Decode(input.Params, &params)
	if err != nil {
		return nil, err
	}
	return a.client.ReadInputRegisters(params.Address, params.Quantity)
}

func (a *Activity) evalReadHoldingRegisters(ctx activity.Context, input *Input) ([]byte, error) {
	params := ReadHoldingRegisters{}
	err := mapstructure.Decode(input.Params, &params)
	if err != nil {
		return nil, err
	}
	return a.client.ReadHoldingRegisters(params.Address, params.Quantity)
}

func (a *Activity) evalWriteSingleRegister(ctx activity.Context, input *Input) ([]byte, error) {
	params := WriteSingleRegister{}
	err := mapstructure.Decode(input.Params, &params)
	if err != nil {
		return nil, err
	}
	return a.client.WriteSingleRegister(params.Address, params.Value)
}

func (a *Activity) evalWriteMultipleRegisters(ctx activity.Context, input *Input) ([]byte, error) {
	params := WriteMultipleRegisters{}
	err := mapstructure.Decode(input.Params, &params)
	if err != nil {
		return nil, err
	}
	return a.client.WriteMultipleRegisters(params.Address, params.Quantity, params.Value)
}

func (a *Activity) evalReadWriteMultipleRegisters(ctx activity.Context, input *Input) ([]byte, error) {
	params := ReadWriteMultipleRegisters{}
	err := mapstructure.Decode(input.Params, &params)
	if err != nil {
		return nil, err
	}
	return a.client.ReadWriteMultipleRegisters(params.ReadAddress, params.ReadQuantity, params.WriteAddress, params.WriteQuantity, params.Value)
}

func (a *Activity) evalMaskWriteRegister(ctx activity.Context, input *Input) ([]byte, error) {
	params := MaskWriteRegister{}
	err := mapstructure.Decode(input.Params, &params)
	if err != nil {
		return nil, err
	}
	return a.client.MaskWriteRegister(params.Address, params.AndMask, params.OrMask)
}

func (a *Activity) evalReadFIFOQueue(ctx activity.Context, input *Input) ([]byte, error) {
	params := ReadFIFOQueue{}
	err := mapstructure.Decode(input.Params, &params)
	if err != nil {
		return nil, err
	}
	return a.client.ReadFIFOQueue(params.Address)
}
